/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.activiti.engine.impl.history;

import java.util.Date;
import java.util.List;
import java.util.Map;

import org.activiti.engine.delegate.event.impl.ActivitiEventBuilder;
import org.activiti.engine.history.HistoricActivityInstance;
import org.activiti.engine.impl.HistoricActivityInstanceQueryImpl;
import org.activiti.engine.impl.cfg.IdGenerator;
import org.activiti.engine.impl.cfg.ProcessEngineConfigurationImpl;
import org.activiti.engine.impl.context.Context;
import org.activiti.engine.impl.identity.Authentication;
import org.activiti.engine.impl.persistence.AbstractManager;
import org.activiti.engine.impl.persistence.entity.CommentEntity;
import org.activiti.engine.impl.persistence.entity.CommentEntityManager;
import org.activiti.engine.impl.persistence.entity.ExecutionEntity;
import org.activiti.engine.impl.persistence.entity.HistoricActivityInstanceEntity;
import org.activiti.engine.impl.persistence.entity.HistoricDetailVariableInstanceUpdateEntity;
import org.activiti.engine.impl.persistence.entity.HistoricFormPropertyEntity;
import org.activiti.engine.impl.persistence.entity.HistoricIdentityLinkEntity;
import org.activiti.engine.impl.persistence.entity.HistoricProcessInstanceEntity;
import org.activiti.engine.impl.persistence.entity.HistoricTaskInstanceEntity;
import org.activiti.engine.impl.persistence.entity.HistoricVariableInstanceEntity;
import org.activiti.engine.impl.persistence.entity.IdentityLinkEntity;
import org.activiti.engine.impl.persistence.entity.TaskEntity;
import org.activiti.engine.impl.persistence.entity.VariableInstanceEntity;
import org.activiti.engine.impl.pvm.process.ActivityImpl;
import org.activiti.engine.impl.pvm.runtime.InterpretableExecution;
import org.activiti.engine.task.Event;
import org.flowable.common.engine.api.delegate.Expression;
import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType;
import org.flowable.common.engine.impl.history.HistoryLevel;
import org.flowable.common.engine.impl.interceptor.EngineConfigurationConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Manager class that centralises recording of all history-related operations that are originated from inside the engine.
 * 
 * @author Frederik Heremans
 * @author Joram Barrez
 */
public class DefaultHistoryManager extends AbstractManager implements HistoryManager {

    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultHistoryManager.class.getName());

    private HistoryLevel historyLevel;

    public DefaultHistoryManager() {
        this.historyLevel = Context.getProcessEngineConfiguration().getHistoryLevel();
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#isHistoryLevelAtLeast(org.activiti5.engine.impl.history.HistoryLevel)
     */
    @Override
    public boolean isHistoryLevelAtLeast(HistoryLevel level) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Current history level: {}, level required: {}", historyLevel, level);
        }
        // Comparing enums actually compares the location of values declared in the enum
        return historyLevel.isAtLeast(level);
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#isHistoryEnabled()
     */
    @Override
    public boolean isHistoryEnabled() {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Current history level: {}", historyLevel);
        }
        return historyLevel != HistoryLevel.NONE;
    }

    // Process related history

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#recordProcessInstanceEnd(java.lang.String, java.lang.String, java.lang.String)
     */
    @Override
    public void recordProcessInstanceEnd(String processInstanceId, String deleteReason, String activityId) {

        if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
            HistoricProcessInstanceEntity historicProcessInstance = getHistoricProcessInstanceManager()
                    .findHistoricProcessInstance(processInstanceId);

            if (historicProcessInstance != null) {
                historicProcessInstance.markEnded(deleteReason);
                historicProcessInstance.setEndActivityId(activityId);

                // Fire event
                ProcessEngineConfigurationImpl config = Context.getProcessEngineConfiguration();
                if (config != null && config.getEventDispatcher().isEnabled()) {
                    config.getEventDispatcher().dispatchEvent(
                            ActivitiEventBuilder.createEntityEvent(FlowableEngineEventType.HISTORIC_PROCESS_INSTANCE_ENDED, historicProcessInstance),
                            EngineConfigurationConstants.KEY_PROCESS_ENGINE_CONFIG);
                }
            }
        }
    }

    @Override
    public void recordProcessInstanceNameChange(String processInstanceId, String newName) {
        if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
            HistoricProcessInstanceEntity historicProcessInstance = getHistoricProcessInstanceManager()
                    .findHistoricProcessInstance(processInstanceId);

            if (historicProcessInstance != null) {
                historicProcessInstance.setName(newName);
            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#recordProcessInstanceStart(org.activiti5.engine.impl.persistence.entity.ExecutionEntity)
     */
    @Override
    public void recordProcessInstanceStart(ExecutionEntity processInstance) {
        if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
            HistoricProcessInstanceEntity historicProcessInstance = new HistoricProcessInstanceEntity(processInstance);

            // Insert historic process-instance
            getDbSqlSession().insert(historicProcessInstance);

            // Fire event
            ProcessEngineConfigurationImpl config = Context.getProcessEngineConfiguration();
            if (config != null && config.getEventDispatcher().isEnabled()) {
                config.getEventDispatcher().dispatchEvent(
                        ActivitiEventBuilder.createEntityEvent(FlowableEngineEventType.HISTORIC_PROCESS_INSTANCE_CREATED, historicProcessInstance),
                        EngineConfigurationConstants.KEY_PROCESS_ENGINE_CONFIG);
            }

            // Also record the start-event manually, as there is no "start" activity history listener for this
            IdGenerator idGenerator = Context.getProcessEngineConfiguration().getIdGenerator();

            String processDefinitionId = processInstance.getProcessDefinitionId();
            String processInstanceId = processInstance.getProcessInstanceId();
            String executionId = processInstance.getId();

            HistoricActivityInstanceEntity historicActivityInstance = new HistoricActivityInstanceEntity();
            historicActivityInstance.setId(idGenerator.getNextId());
            historicActivityInstance.setProcessDefinitionId(processDefinitionId);
            historicActivityInstance.setProcessInstanceId(processInstanceId);
            historicActivityInstance.setExecutionId(executionId);
            historicActivityInstance.setActivityId(processInstance.getActivityId());
            historicActivityInstance.setActivityName((String) processInstance.getActivity().getProperty("name"));
            historicActivityInstance.setActivityType((String) processInstance.getActivity().getProperty("type"));
            Date now = Context.getProcessEngineConfiguration().getClock().getCurrentTime();
            historicActivityInstance.setStartTime(now);

            // Inherit tenant id (if applicable)
            if (processInstance.getTenantId() != null) {
                historicActivityInstance.setTenantId(processInstance.getTenantId());
            }

            getDbSqlSession().insert(historicActivityInstance);

            // Fire event
            if (config != null && config.getEventDispatcher().isEnabled()) {
                config.getEventDispatcher().dispatchEvent(
                        ActivitiEventBuilder.createEntityEvent(FlowableEngineEventType.HISTORIC_ACTIVITY_INSTANCE_CREATED, historicActivityInstance),
                        EngineConfigurationConstants.KEY_PROCESS_ENGINE_CONFIG);
            }

        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#recordSubProcessInstanceStart(org.activiti5.engine.impl.persistence.entity.ExecutionEntity,
     * org.activiti5.engine.impl.persistence.entity.ExecutionEntity)
     */
    @Override
    public void recordSubProcessInstanceStart(ExecutionEntity parentExecution, ExecutionEntity subProcessInstance) {
        if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {

            HistoricProcessInstanceEntity historicProcessInstance = new HistoricProcessInstanceEntity(subProcessInstance);

            ActivityImpl initialActivity = subProcessInstance.getActivity();
            // Fix for ACT-1728: startActivityId not initialized with subprocess-instance
            if (historicProcessInstance.getStartActivityId() == null) {
                historicProcessInstance.setStartActivityId(subProcessInstance.getProcessDefinition().getInitial().getId());
                initialActivity = subProcessInstance.getProcessDefinition().getInitial();
            }
            getDbSqlSession().insert(historicProcessInstance);

            // Fire event
            ProcessEngineConfigurationImpl config = Context.getProcessEngineConfiguration();
            if (config != null && config.getEventDispatcher().isEnabled()) {
                config.getEventDispatcher().dispatchEvent(
                        ActivitiEventBuilder.createEntityEvent(FlowableEngineEventType.HISTORIC_PROCESS_INSTANCE_CREATED, historicProcessInstance),
                        EngineConfigurationConstants.KEY_PROCESS_ENGINE_CONFIG);
            }

            HistoricActivityInstanceEntity activityInstance = findActivityInstance(parentExecution);
            if (activityInstance != null) {
                activityInstance.setCalledProcessInstanceId(subProcessInstance.getProcessInstanceId());
            }

            // Fix for ACT-1728: start-event not recorded for subprocesses
            IdGenerator idGenerator = Context.getProcessEngineConfiguration().getIdGenerator();

            // Also record the start-event manually, as there is no "start" activity history listener for this
            HistoricActivityInstanceEntity historicActivityInstance = new HistoricActivityInstanceEntity();
            historicActivityInstance.setId(idGenerator.getNextId());
            historicActivityInstance.setProcessDefinitionId(subProcessInstance.getProcessDefinitionId());
            historicActivityInstance.setProcessInstanceId(subProcessInstance.getProcessInstanceId());
            historicActivityInstance.setExecutionId(subProcessInstance.getId());
            historicActivityInstance.setActivityId(initialActivity.getId());
            historicActivityInstance.setActivityName((String) initialActivity.getProperty("name"));
            historicActivityInstance.setActivityType((String) initialActivity.getProperty("type"));
            Date now = Context.getProcessEngineConfiguration().getClock().getCurrentTime();
            historicActivityInstance.setStartTime(now);

            getDbSqlSession().insert(historicActivityInstance);

            // Fire event
            if (config != null && config.getEventDispatcher().isEnabled()) {
                config.getEventDispatcher().dispatchEvent(
                        ActivitiEventBuilder.createEntityEvent(FlowableEngineEventType.HISTORIC_ACTIVITY_INSTANCE_CREATED, historicActivityInstance),
                        EngineConfigurationConstants.KEY_PROCESS_ENGINE_CONFIG);
            }

        }
    }

    // Activity related history

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#recordActivityStart(org.activiti5.engine.impl.persistence.entity.ExecutionEntity)
     */
    @Override
    public void recordActivityStart(ExecutionEntity executionEntity) {
        if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
            if (executionEntity.getActivity() != null) {
                IdGenerator idGenerator = Context.getProcessEngineConfiguration().getIdGenerator();

                String processDefinitionId = executionEntity.getProcessDefinitionId();
                String processInstanceId = executionEntity.getProcessInstanceId();
                String executionId = executionEntity.getId();

                HistoricActivityInstanceEntity historicActivityInstance = new HistoricActivityInstanceEntity();
                historicActivityInstance.setId(idGenerator.getNextId());
                historicActivityInstance.setProcessDefinitionId(processDefinitionId);
                historicActivityInstance.setProcessInstanceId(processInstanceId);
                historicActivityInstance.setExecutionId(executionId);
                historicActivityInstance.setActivityId(executionEntity.getActivityId());
                historicActivityInstance.setActivityName((String) executionEntity.getActivity().getProperty("name"));
                historicActivityInstance.setActivityType((String) executionEntity.getActivity().getProperty("type"));
                historicActivityInstance.setStartTime(Context.getProcessEngineConfiguration().getClock().getCurrentTime());

                // Inherit tenant id (if applicable)
                if (executionEntity.getTenantId() != null) {
                    historicActivityInstance.setTenantId(executionEntity.getTenantId());
                }

                getDbSqlSession().insert(historicActivityInstance);

                // Fire event
                ProcessEngineConfigurationImpl config = Context.getProcessEngineConfiguration();
                if (config != null && config.getEventDispatcher().isEnabled()) {
                    config.getEventDispatcher().dispatchEvent(
                            ActivitiEventBuilder.createEntityEvent(FlowableEngineEventType.HISTORIC_ACTIVITY_INSTANCE_CREATED, historicActivityInstance),
                            EngineConfigurationConstants.KEY_PROCESS_ENGINE_CONFIG);
                }

            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#recordActivityEnd (org.activiti5.engine.impl.persistence.entity.ExecutionEntity)
     */
    @Override
    public void recordActivityEnd(ExecutionEntity executionEntity) {
        if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
            HistoricActivityInstanceEntity historicActivityInstance = findActivityInstance(executionEntity);
            if (historicActivityInstance != null) {
                endHistoricActivityInstance(historicActivityInstance);
            }
        }
    }

    protected void endHistoricActivityInstance(HistoricActivityInstanceEntity historicActivityInstance) {
        historicActivityInstance.markEnded(null);

        // Fire event
        ProcessEngineConfigurationImpl config = Context.getProcessEngineConfiguration();
        if (config != null && config.getEventDispatcher().isEnabled()) {
            config.getEventDispatcher().dispatchEvent(
                    ActivitiEventBuilder.createEntityEvent(FlowableEngineEventType.HISTORIC_ACTIVITY_INSTANCE_ENDED, historicActivityInstance),
                    EngineConfigurationConstants.KEY_PROCESS_ENGINE_CONFIG);
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#recordStartEventEnded(java.lang.String, java.lang.String)
     */
    @Override
    public void recordStartEventEnded(ExecutionEntity execution, String activityId) {
        if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {

            // Interrupted executions might not have an activityId set, skip recording history.
            if (activityId == null) {
                return;
            }

            HistoricActivityInstanceEntity historicActivityInstance = findActivityInstance(execution, activityId, false); // false -> no need to check the persistent store, as process just started
            if (historicActivityInstance != null) {
                endHistoricActivityInstance(historicActivityInstance);
            }
        }
    }

    @Override
    public HistoricActivityInstanceEntity findActivityInstance(ExecutionEntity execution) {
        return findActivityInstance(execution, execution.getActivityId(), true);
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti.engine.impl.history.HistoryManagerInterface#findActivityInstance (org.activiti.engine.impl.persistence.entity.ExecutionEntity)
     */
    protected HistoricActivityInstanceEntity findActivityInstance(ExecutionEntity execution, String activityId, boolean checkPersistentStore) {

        String executionId = execution.getId();

        // search for the historic activity instance in the dbsqlsession cache
        List<HistoricActivityInstanceEntity> cachedHistoricActivityInstances = getDbSqlSession()
                .findInCache(HistoricActivityInstanceEntity.class);
        for (HistoricActivityInstanceEntity cachedHistoricActivityInstance : cachedHistoricActivityInstances) {
            if (executionId.equals(cachedHistoricActivityInstance.getExecutionId())
                    && activityId != null
                    && (activityId.equals(cachedHistoricActivityInstance.getActivityId()))
                    && (cachedHistoricActivityInstance.getEndTime() == null)) {
                return cachedHistoricActivityInstance;
            }
        }

        List<HistoricActivityInstance> historicActivityInstances = null;
        if (checkPersistentStore) {
            historicActivityInstances = new HistoricActivityInstanceQueryImpl(Context.getCommandContext())
                    .executionId(executionId)
                    .activityId(activityId)
                    .unfinished()
                    .listPage(0, 1);
        }

        if (historicActivityInstances != null && !historicActivityInstances.isEmpty()) {
            return (HistoricActivityInstanceEntity) historicActivityInstances.get(0);
        }

        if (execution.getParentId() != null) {
            return findActivityInstance(execution.getParent(), activityId, checkPersistentStore);
        }

        return null;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#recordExecutionReplacedBy(org.activiti5.engine.impl.persistence.entity.ExecutionEntity,
     * org.activiti5.engine.impl.pvm.runtime.InterpretableExecution)
     */
    @Override
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public void recordExecutionReplacedBy(ExecutionEntity execution, InterpretableExecution replacedBy) {
        if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {

            // Update the cached historic activity instances that are open
            List<HistoricActivityInstanceEntity> cachedHistoricActivityInstances = getDbSqlSession().findInCache(HistoricActivityInstanceEntity.class);
            for (HistoricActivityInstanceEntity cachedHistoricActivityInstance : cachedHistoricActivityInstances) {
                if ((cachedHistoricActivityInstance.getEndTime() == null)
                        && (execution.getId().equals(cachedHistoricActivityInstance.getExecutionId()))) {
                    cachedHistoricActivityInstance.setExecutionId(replacedBy.getId());
                }
            }

            // Update the persisted historic activity instances that are open
            List<HistoricActivityInstanceEntity> historicActivityInstances = (List) new HistoricActivityInstanceQueryImpl(Context.getCommandContext())
                    .executionId(execution.getId())
                    .unfinished()
                    .list();
            for (HistoricActivityInstanceEntity historicActivityInstance : historicActivityInstances) {
                historicActivityInstance.setExecutionId(replacedBy.getId());
            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#recordProcessDefinitionChange(java.lang.String, java.lang.String)
     */
    @Override
    public void recordProcessDefinitionChange(String processInstanceId, String processDefinitionId) {
        if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
            HistoricProcessInstanceEntity historicProcessInstance = getHistoricProcessInstanceManager().findHistoricProcessInstance(processInstanceId);
            if (historicProcessInstance != null) {
                historicProcessInstance.setProcessDefinitionId(processDefinitionId);
            }
        }
    }

    // Task related history

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#recordTaskCreated(org.activiti5.engine.impl.persistence.entity.TaskEntity,
     * org.activiti5.engine.impl.persistence.entity.ExecutionEntity)
     */
    @Override
    public void recordTaskCreated(TaskEntity task, ExecutionEntity execution) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            HistoricTaskInstanceEntity historicTaskInstance = new HistoricTaskInstanceEntity(task, execution);
            getDbSqlSession().insert(historicTaskInstance);
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#recordTaskAssignment(org.activiti5.engine.impl.persistence.entity.TaskEntity)
     */
    @Override
    public void recordTaskAssignment(TaskEntity task) {
        ExecutionEntity executionEntity = task.getExecution();
        if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
            if (executionEntity != null) {
                HistoricActivityInstanceEntity historicActivityInstance = findActivityInstance(executionEntity);
                if (historicActivityInstance != null) {
                    historicActivityInstance.setAssignee(task.getAssignee());
                }
            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#recordTaskClaim(java.lang.String)
     */

    @Override
    public void recordTaskClaim(String taskId) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            HistoricTaskInstanceEntity historicTaskInstance = getDbSqlSession().selectById(HistoricTaskInstanceEntity.class, taskId);
            if (historicTaskInstance != null) {
                historicTaskInstance.setClaimTime(Context.getProcessEngineConfiguration().getClock().getCurrentTime());
            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#recordTaskId(org.activiti5.engine.impl.persistence.entity.TaskEntity)
     */
    @Override
    public void recordTaskId(TaskEntity task) {
        if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
            ExecutionEntity execution = task.getExecution();
            if (execution != null) {
                HistoricActivityInstanceEntity historicActivityInstance = findActivityInstance(execution);
                if (historicActivityInstance != null) {
                    historicActivityInstance.setTaskId(task.getId());
                }
            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#recordTaskEnd(java.lang.String, java.lang.String)
     */
    @Override
    public void recordTaskEnd(String taskId, String deleteReason) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            HistoricTaskInstanceEntity historicTaskInstance = getDbSqlSession().selectById(HistoricTaskInstanceEntity.class, taskId);
            if (historicTaskInstance != null) {
                historicTaskInstance.markEnded(deleteReason);
            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#recordTaskAssigneeChange(java.lang.String, java.lang.String)
     */
    @Override
    public void recordTaskAssigneeChange(String taskId, String assignee) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            HistoricTaskInstanceEntity historicTaskInstance = getDbSqlSession().selectById(HistoricTaskInstanceEntity.class, taskId);
            if (historicTaskInstance != null) {
                historicTaskInstance.setAssignee(assignee);
            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#recordTaskOwnerChange(java.lang.String, java.lang.String)
     */
    @Override
    public void recordTaskOwnerChange(String taskId, String owner) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            HistoricTaskInstanceEntity historicTaskInstance = getDbSqlSession().selectById(HistoricTaskInstanceEntity.class, taskId);
            if (historicTaskInstance != null) {
                historicTaskInstance.setOwner(owner);
            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#recordTaskNameChange(java.lang.String, java.lang.String)
     */
    @Override
    public void recordTaskNameChange(String taskId, String taskName) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            HistoricTaskInstanceEntity historicTaskInstance = getDbSqlSession().selectById(HistoricTaskInstanceEntity.class, taskId);
            if (historicTaskInstance != null) {
                historicTaskInstance.setName(taskName);
            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#recordTaskDescriptionChange(java.lang.String, java.lang.String)
     */
    @Override
    public void recordTaskDescriptionChange(String taskId, String description) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            HistoricTaskInstanceEntity historicTaskInstance = getDbSqlSession().selectById(HistoricTaskInstanceEntity.class, taskId);
            if (historicTaskInstance != null) {
                historicTaskInstance.setDescription(description);
            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#recordTaskDueDateChange(java.lang.String, java.util.Date)
     */
    @Override
    public void recordTaskDueDateChange(String taskId, Date dueDate) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            HistoricTaskInstanceEntity historicTaskInstance = getDbSqlSession().selectById(HistoricTaskInstanceEntity.class, taskId);
            if (historicTaskInstance != null) {
                historicTaskInstance.setDueDate(dueDate);
            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#recordTaskPriorityChange(java.lang.String, int)
     */
    @Override
    public void recordTaskPriorityChange(String taskId, int priority) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            HistoricTaskInstanceEntity historicTaskInstance = getDbSqlSession().selectById(HistoricTaskInstanceEntity.class, taskId);
            if (historicTaskInstance != null) {
                historicTaskInstance.setPriority(priority);
            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#recordTaskCategoryChange(java.lang.String, java.lang.String)
     */
    @Override
    public void recordTaskCategoryChange(String taskId, String category) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            HistoricTaskInstanceEntity historicTaskInstance = getDbSqlSession().selectById(HistoricTaskInstanceEntity.class, taskId);
            if (historicTaskInstance != null) {
                historicTaskInstance.setCategory(category);
            }
        }
    }

    @Override
    public void recordTaskFormKeyChange(String taskId, String formKey) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            HistoricTaskInstanceEntity historicTaskInstance = getDbSqlSession().selectById(HistoricTaskInstanceEntity.class, taskId);
            if (historicTaskInstance != null) {
                historicTaskInstance.setFormKey(formKey);
            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#recordTaskParentTaskIdChange(java.lang.String, java.lang.String)
     */
    @Override
    public void recordTaskParentTaskIdChange(String taskId, String parentTaskId) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            HistoricTaskInstanceEntity historicTaskInstance = getDbSqlSession().selectById(HistoricTaskInstanceEntity.class, taskId);
            if (historicTaskInstance != null) {
                historicTaskInstance.setParentTaskId(parentTaskId);
            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#recordTaskExecutionIdChange(java.lang.String, java.lang.String)
     */
    @Override
    public void recordTaskExecutionIdChange(String taskId, String executionId) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            HistoricTaskInstanceEntity historicTaskInstance = getDbSqlSession().selectById(HistoricTaskInstanceEntity.class, taskId);
            if (historicTaskInstance != null) {
                historicTaskInstance.setExecutionId(executionId);
            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#recordTaskDefinitionKeyChange(org.activiti5.engine.impl.persistence.entity.TaskEntity, java.lang.String)
     */
    @Override
    public void recordTaskDefinitionKeyChange(TaskEntity task, String taskDefinitionKey) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            HistoricTaskInstanceEntity historicTaskInstance = getDbSqlSession().selectById(HistoricTaskInstanceEntity.class, task.getId());
            if (historicTaskInstance != null) {
                historicTaskInstance.setTaskDefinitionKey(taskDefinitionKey);

                if (taskDefinitionKey != null) {
                    Expression taskFormExpression = task.getTaskDefinition().getFormKeyExpression();
                    if (taskFormExpression != null) {
                        Object formValue = taskFormExpression.getValue(task.getExecution());
                        if (formValue != null) {
                            historicTaskInstance.setFormKey(formValue.toString());
                        }
                    }
                }
            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti.engine.impl.history.HistoryManagerInterface#recordTaskProcessDefinitionChange(java.lang.String, java.lang.String)
     */
    @Override
    public void recordTaskProcessDefinitionChange(String taskId, String processDefinitionId) {
        if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
            HistoricTaskInstanceEntity historicTaskInstance = getDbSqlSession().selectById(HistoricTaskInstanceEntity.class, taskId);
            if (historicTaskInstance != null) {
                historicTaskInstance.setProcessDefinitionId(processDefinitionId);
            }
        }
    }

    // Variables related history

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#recordVariableCreate(org.activiti5.engine.impl.persistence.entity.VariableInstanceEntity)
     */
    @Override
    public void recordVariableCreate(VariableInstanceEntity variable) {
        // Historic variables
        if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
            HistoricVariableInstanceEntity.copyAndInsert(variable);
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#recordHistoricDetailVariableCreate(org.activiti5.engine.impl.persistence.entity.VariableInstanceEntity,
     * org.activiti5.engine.impl.persistence.entity.ExecutionEntity, boolean)
     */
    @Override
    public void recordHistoricDetailVariableCreate(VariableInstanceEntity variable, ExecutionEntity sourceActivityExecution, boolean useActivityId) {
        if (isHistoryLevelAtLeast(HistoryLevel.FULL)) {

            HistoricDetailVariableInstanceUpdateEntity historicVariableUpdate = HistoricDetailVariableInstanceUpdateEntity.copyAndInsert(variable);

            if (useActivityId && sourceActivityExecution != null) {
                HistoricActivityInstanceEntity historicActivityInstance = findActivityInstance(sourceActivityExecution);
                if (historicActivityInstance != null) {
                    historicVariableUpdate.setActivityInstanceId(historicActivityInstance.getId());
                }
            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#recordVariableUpdate (org.activiti5.engine.impl.persistence.entity.VariableInstanceEntity)
     */
    @Override
    public void recordVariableUpdate(VariableInstanceEntity variable) {
        if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
            HistoricVariableInstanceEntity historicProcessVariable = getDbSqlSession()
                    .findInCache(HistoricVariableInstanceEntity.class, variable.getId());
            if (historicProcessVariable == null) {
                historicProcessVariable = Context.getCommandContext()
                        .getHistoricVariableInstanceEntityManager()
                        .findHistoricVariableInstanceByVariableInstanceId(variable.getId());
            }

            if (historicProcessVariable != null) {
                historicProcessVariable.copyValue(variable);
            } else {
                HistoricVariableInstanceEntity.copyAndInsert(variable);
            }
        }
    }

    @Override
    public void recordVariableRemoved(VariableInstanceEntity variable) {
        if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
            HistoricVariableInstanceEntity historicProcessVariable = getDbSqlSession()
                    .findInCache(HistoricVariableInstanceEntity.class, variable.getId());
            if (historicProcessVariable == null) {
                historicProcessVariable = Context.getCommandContext()
                        .getHistoricVariableInstanceEntityManager()
                        .findHistoricVariableInstanceByVariableInstanceId(variable.getId());
            }

            if (historicProcessVariable != null) {
                Context.getCommandContext()
                        .getHistoricVariableInstanceEntityManager()
                        .delete(historicProcessVariable);
            }
        }
    }

    // Comment related history

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#createIdentityLinkComment(java.lang.String, java.lang.String, java.lang.String, java.lang.String, boolean)
     */
    @Override
    public void createIdentityLinkComment(String taskId, String userId, String groupId, String type, boolean create) {
        createIdentityLinkComment(taskId, userId, groupId, type, create, false);
    }

    @Override
    public void createUserIdentityLinkComment(String taskId, String userId, String type, boolean create) {
        createIdentityLinkComment(taskId, userId, null, type, create, false);
    }

    @Override
    public void createGroupIdentityLinkComment(String taskId, String groupId, String type, boolean create) {
        createIdentityLinkComment(taskId, null, groupId, type, create, false);
    }

    @Override
    public void createUserIdentityLinkComment(String taskId, String userId, String type, boolean create, boolean forceNullUserId) {
        createIdentityLinkComment(taskId, userId, null, type, create, forceNullUserId);
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#createIdentityLinkComment(java.lang.String, java.lang.String, java.lang.String, java.lang.String, boolean, boolean)
     */
    @Override
    public void createIdentityLinkComment(String taskId, String userId, String groupId, String type, boolean create, boolean forceNullUserId) {
        if (isHistoryEnabled()) {
            String authenticatedUserId = Authentication.getAuthenticatedUserId();
            CommentEntity comment = new CommentEntity();
            comment.setUserId(authenticatedUserId);
            comment.setType(CommentEntity.TYPE_EVENT);
            comment.setTime(Context.getProcessEngineConfiguration().getClock().getCurrentTime());
            comment.setTaskId(taskId);
            if (userId != null || forceNullUserId) {
                if (create) {
                    comment.setAction(Event.ACTION_ADD_USER_LINK);
                } else {
                    comment.setAction(Event.ACTION_DELETE_USER_LINK);
                }
                comment.setMessage(new String[] { userId, type });
            } else {
                if (create) {
                    comment.setAction(Event.ACTION_ADD_GROUP_LINK);
                } else {
                    comment.setAction(Event.ACTION_DELETE_GROUP_LINK);
                }
                comment.setMessage(new String[] { groupId, type });
            }
            getSession(CommentEntityManager.class).insert(comment);
        }
    }

    @Override
    public void createProcessInstanceIdentityLinkComment(String processInstanceId, String userId, String groupId, String type, boolean create) {
        createProcessInstanceIdentityLinkComment(processInstanceId, userId, groupId, type, create, false);
    }

    @Override
    public void createProcessInstanceIdentityLinkComment(String processInstanceId, String userId, String groupId, String type, boolean create, boolean forceNullUserId) {
        if (isHistoryEnabled()) {
            String authenticatedUserId = Authentication.getAuthenticatedUserId();
            CommentEntity comment = new CommentEntity();
            comment.setUserId(authenticatedUserId);
            comment.setType(CommentEntity.TYPE_EVENT);
            comment.setTime(Context.getProcessEngineConfiguration().getClock().getCurrentTime());
            comment.setProcessInstanceId(processInstanceId);
            if (userId != null || forceNullUserId) {
                if (create) {
                    comment.setAction(Event.ACTION_ADD_USER_LINK);
                } else {
                    comment.setAction(Event.ACTION_DELETE_USER_LINK);
                }
                comment.setMessage(new String[] { userId, type });
            } else {
                if (create) {
                    comment.setAction(Event.ACTION_ADD_GROUP_LINK);
                } else {
                    comment.setAction(Event.ACTION_DELETE_GROUP_LINK);
                }
                comment.setMessage(new String[] { groupId, type });
            }
            getSession(CommentEntityManager.class).insert(comment);
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#createAttachmentComment(java.lang.String, java.lang.String, java.lang.String, boolean)
     */
    @Override
    public void createAttachmentComment(String taskId, String processInstanceId, String attachmentName, boolean create) {
        if (isHistoryEnabled()) {
            String userId = Authentication.getAuthenticatedUserId();
            CommentEntity comment = new CommentEntity();
            comment.setUserId(userId);
            comment.setType(CommentEntity.TYPE_EVENT);
            comment.setTime(Context.getProcessEngineConfiguration().getClock().getCurrentTime());
            comment.setTaskId(taskId);
            comment.setProcessInstanceId(processInstanceId);
            if (create) {
                comment.setAction(Event.ACTION_ADD_ATTACHMENT);
            } else {
                comment.setAction(Event.ACTION_DELETE_ATTACHMENT);
            }
            comment.setMessage(attachmentName);
            getSession(CommentEntityManager.class).insert(comment);
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#reportFormPropertiesSubmitted(org.activiti5.engine.impl.persistence.entity.ExecutionEntity, java.util.Map, java.lang.String)
     */
    @Override
    public void reportFormPropertiesSubmitted(ExecutionEntity processInstance, Map<String, String> properties, String taskId) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            for (String propertyId : properties.keySet()) {
                String propertyValue = properties.get(propertyId);
                HistoricFormPropertyEntity historicFormProperty = new HistoricFormPropertyEntity(processInstance, propertyId, propertyValue, taskId);
                getDbSqlSession().insert(historicFormProperty);
            }
        }
    }

    // Identity link related history
    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#recordIdentityLinkCreated(org.activiti5.engine.impl.persistence.entity.IdentityLinkEntity)
     */
    @Override
    public void recordIdentityLinkCreated(IdentityLinkEntity identityLink) {
        // It makes no sense storing historic counterpart for an identity-link that is related
        // to a process-definition only as this is never kept in history
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT) && (identityLink.getProcessInstanceId() != null || identityLink.getTaskId() != null)) {
            HistoricIdentityLinkEntity historicIdentityLinkEntity = new HistoricIdentityLinkEntity(identityLink);
            getDbSqlSession().insert(historicIdentityLinkEntity);
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#deleteHistoricIdentityLink(java.lang.String)
     */
    @Override
    public void deleteHistoricIdentityLink(String id) {
        if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
            getHistoricIdentityLinkEntityManager().deleteHistoricIdentityLink(id);
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.activiti5.engine.impl.history.HistoryManagerInterface#updateProcessBusinessKeyInHistory(org.activiti5.engine.impl.persistence.entity.ExecutionEntity)
     */
    @Override
    public void updateProcessBusinessKeyInHistory(ExecutionEntity processInstance) {
        if (isHistoryEnabled()) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("updateProcessBusinessKeyInHistory : {}", processInstance.getId());
            }
            if (processInstance != null) {
                HistoricProcessInstanceEntity historicProcessInstance = getDbSqlSession().selectById(HistoricProcessInstanceEntity.class, processInstance.getId());
                if (historicProcessInstance != null) {
                    historicProcessInstance.setBusinessKey(processInstance.getProcessBusinessKey());
                    getDbSqlSession().update(historicProcessInstance);
                }
            }
        }
    }
}
